# uncompyle6 version 3.2.3
# Python bytecode 3.6 (3379)
# Decompiled from: Python 3.6.8 |Anaconda custom (64-bit)| (default, Feb 21 2019, 18:30:04) [MSC v.1916 64 bit (AMD64)]
# Embedded file name: site-packages\PyInstaller\building\imphook.py
"""
Code related to processing of import hooks.
"""
import glob, sys, weakref, os.path
from .. import log as logging
from ..compat import expand_path, importlib_load_source, FileNotFoundError, UserDict
from .imphookapi import PostGraphAPI
from .utils import format_binaries_and_datas

logger = logging.getLogger(__name__)


class ModuleHookCache(UserDict):
    """
    Cache of lazily loadable hook script objects.
    
    This cache is implemented as a `dict` subclass mapping from the
    fully-qualified names of all modules with at least one hook script to lists
    of `ModuleHook` instances encapsulating these scripts. As a `dict` subclass,
    all cached module names and hook scripts are accessible via standard
    dictionary operations.
    
    Attributes
    ----------
    module_graph : ModuleGraph
        Current module graph.
    _hook_module_name_prefix : str
        String prefixing the names of all in-memory modules lazily loaded from
        cached hook scripts. See also the `hook_module_name_prefix` parameter
        passed to the `ModuleHook.__init__()` method.
    """

    _cache_id_next = 0

    def __init__(self, module_graph, hook_dirs):
        """
        Cache all hook scripts in the passed directories.
        
        **Order of caching is significant** with respect to hooks for the same
        module, as the values of this dictionary are lists. Hooks for the same
        module will be run in the order in which they are cached. Previously
        cached hooks are always preserved rather than overidden.
        
        By default, official hooks are cached _before_ user-defined hooks. For
        modules with both official and user-defined hooks, this implies that the
        former take priority over and hence will be loaded _before_ the latter.
        
        Parameters
        ----------
        module_graph : ModuleGraph
            Current module graph.
        hook_dirs : list
            List of the absolute or relative paths of all directories containing
            **hook scripts** (i.e., Python scripts with filenames matching
            `hook-{module_name}.py`, where `{module_name}` is the module hooked
            by that script) to be cached.
        """
        UserDict.__init__(self)
        self.module_graph = weakref.proxy(module_graph)
        self._hook_module_name_prefix = ("__PyInstaller_hooks_{}_").format(
            ModuleHookCache._cache_id_next
        )
        ModuleHookCache._cache_id_next += 1
        self._cache_hook_dirs(hook_dirs)

    def _cache_hook_dirs(self, hook_dirs):
        """
        Cache all hook scripts in the passed directories.
        
        Parameters
        ----------
        hook_dirs : list
            List of the absolute or relative paths of all directories containing
            hook scripts to be cached.
        """
        for hook_dir in hook_dirs:
            hook_dir = os.path.abspath(expand_path(hook_dir))
            if not os.path.isdir(hook_dir):
                raise FileNotFoundError(
                    ('Hook directory "{}" not found.').format(hook_dir)
                )
            hook_filenames = glob.glob(os.path.join(hook_dir, "hook-*.py"))
            for hook_filename in hook_filenames:
                module_name = os.path.basename(hook_filename)[5:-3]
                module_hook = ModuleHook(
                    module_graph=self.module_graph,
                    module_name=module_name,
                    hook_filename=hook_filename,
                    hook_module_name_prefix=self._hook_module_name_prefix,
                )
                module_hooks = self.setdefault(module_name, [])
                module_hooks.append(module_hook)

    def remove_modules(self, *module_names):
        """
        Remove the passed modules and all hook scripts cached for these modules
        from this cache.
        
        Parameters
        ----------
        module_names : list
            List of all fully-qualified module names to be removed.
        """
        for module_name in module_names:
            module_hooks = self.get(module_name, [])
            for module_hook in module_hooks:
                sys.modules.pop(module_hook.hook_module_name, None)

            self.pop(module_name, None)


_MAGIC_MODULE_HOOK_ATTRS = {
    "datas": (set, format_binaries_and_datas),
    "binaries": (set, format_binaries_and_datas),
    "excludedimports": (set, None),
    "hiddenimports": (list, None),
}


class ModuleHook(object):
    """
    Cached object encapsulating a lazy loadable hook script.
    
    This object exposes public attributes (e.g., `datas`) of the underlying hook
    script as attributes of the same name of this object. On the first access of
    any such attribute, this hook script is lazily loaded into an in-memory
    private module reused on subsequent accesses. These dynamic attributes are
    referred to as "magic." All other static attributes of this object (e.g.,
    `hook_module_name`) are referred to as "non-magic."
    
    Attributes (Magic)
    ----------
    datas : set
        Set of `TOC`-style 2-tuples `(target_file, source_file)` for all
        external non-executable files required by the module being hooked,
        converted from the `datas` list of hook-style 2-tuples
        `(source_dir_or_glob, target_dir)` defined by this hook script.
    binaries : set
        Set of `TOC`-style 2-tuples `(target_file, source_file)` for all
        external executable files required by the module being hooked, converted
        from the `binaries` list of hook-style 2-tuples
        `(source_dir_or_glob, target_dir)` defined by this hook script.
    excludedimports : set
        Set of the fully-qualified names of all modules imported by the module
        being hooked to be ignored rather than imported from that module,
        converted from the `excludedimports` list defined by this hook script.
        These modules will only be "locally" rather than "globally" ignored.
        These modules will remain importable from all modules other than the
        module being hooked.
    hiddenimports : set
        Set of the fully-qualified names of all modules imported by the module
        being hooked that are _not_ automatically detectable by PyInstaller
        (usually due to being dynamically imported in that module), converted
        from the `hiddenimports` list defined by this hook script.
    
    Attributes (Non-magic)
    ----------
    module_graph : ModuleGraph
        Current module graph.
    module_name : str
        Name of the module hooked by this hook script.
    hook_filename : str
        Absolute or relative path of this hook script.
    hook_module_name : str
        Name of the in-memory module of this hook script's interpreted contents.
    _hook_module : module
        In-memory module of this hook script's interpreted contents, lazily
        loaded on the first call to the `_load_hook_module()` method _or_ `None`
        if this method has yet to be accessed.
    """

    def __init__(
        self, module_graph, module_name, hook_filename, hook_module_name_prefix
    ):
        """
        Initialize this metadata.
        
        Parameters
        ----------
        module_graph : ModuleGraph
            Current module graph.
        module_name : str
            Name of the module hooked by this hook script.
        hook_filename : str
            Absolute or relative path of this hook script.
        hook_module_name_prefix : str
            String prefixing the name of the in-memory module for this hook
            script. To avoid namespace clashes with similar modules created by
            other `ModuleHook` objects in other `ModuleHookCache` containers,
            this string _must_ be unique to the `ModuleHookCache` container
            containing this `ModuleHook` object. If this string is non-unique,
            an existing in-memory module will be erroneously reused when lazily
            loading this hook script, thus erroneously resanitizing previously
            sanitized hook script attributes (e.g., `datas`) with the
            `format_binaries_and_datas()` helper.
        """
        if not isinstance(module_graph, weakref.ProxyTypes):
            raise AssertionError
        self.module_graph = module_graph
        self.module_name = module_name
        self.hook_filename = hook_filename
        self.hook_module_name = hook_module_name_prefix + self.module_name.replace(
            ".", "_"
        )
        self._hook_module = None

    def __getattr__(self, attr_name):
        """
        Get the magic attribute with the passed name (e.g., `datas`) from this
        lazily loaded hook script if any _or_ raise `AttributeError` otherwise.
        
        This special method is called only for attributes _not_ already defined
        by this object. This includes undefined attributes and the first attempt
        to access magic attributes.
        
        This special method is _not_ called for subsequent attempts to access
        magic attributes. The first attempt to access magic attributes defines
        corresponding instance variables accessible via the `self.__dict__`
        instance dictionary (e.g., as `self.datas`) without calling this method.
        This approach also allows magic attributes to be deleted from this
        object _without_ defining the `__delattr__()` special method.
        
        See Also
        ----------
        Class docstring for supported magic attributes.
        """
        if attr_name in _MAGIC_MODULE_HOOK_ATTRS:
            self._load_hook_module()
            return super(ModuleHook, self).__getattr__(attr_name)
        raise AttributeError(attr_name)

    def __setattr__(self, attr_name, attr_value):
        """
        Set the attribute with the passed name to the passed value.
        
        If this is a magic attribute, this hook script will be lazily loaded
        before setting this attribute. Unlike `__getattr__()`, this special
        method is called to set _any_ attribute -- including magic, non-magic,
        and undefined attributes.
        
        See Also
        ----------
        Class docstring for supported magic attributes.
        """
        if attr_name in _MAGIC_MODULE_HOOK_ATTRS:
            self._load_hook_module()
        return super(ModuleHook, self).__setattr__(attr_name, attr_value)

    def _load_hook_module(self):
        """
        Lazily load this hook script into an in-memory private module.
        
        This method (and, indeed, this class) preserves all attributes and
        functions defined by this hook script as is, ensuring sane behaviour in
        hook functions _not_ expecting unplanned external modification. Instead,
        this method copies public attributes defined by this hook script
        (e.g., `binaries`) into private attributes of this object, which the
        special `__getattr__()` and `__setattr__()` methods safely expose to
        external callers. For public attributes _not_ defined by this hook
        script, the corresponding private attributes will be assigned sane
        defaults. For some public attributes defined by this hook script, the
        corresponding private attributes will be transformed into objects more
        readily and safely consumed elsewhere by external callers.
        
        See Also
        ----------
        Class docstring for supported attributes.
        """
        if self._hook_module is not None:
            return
        logger.info('Loading module hook "%s"...', os.path.basename(self.hook_filename))
        self._hook_module = importlib_load_source(
            self.hook_module_name, self.hook_filename
        )
        for (
            attr_name,
            (default_type, sanitizer_func),
        ) in _MAGIC_MODULE_HOOK_ATTRS.items():
            attr_value = getattr(self._hook_module, attr_name, None)
            if attr_value is None:
                attr_value = default_type()
            else:
                if sanitizer_func is not None:
                    attr_value = sanitizer_func(attr_value)
            setattr(self, attr_name, attr_value)

    def post_graph(self):
        """
        Call the **post-graph hook** (i.e., `hook()` function) defined by this
        hook script if any.
        
        This method is intended to be called _after_ the module graph for this
        application is constructed.
        """
        self._load_hook_module()
        self._process_hook_func()
        self._process_hidden_imports()
        self._process_excluded_imports()

    def _process_hook_func(self):
        """
        Call this hook's `hook()` function if defined.
        """
        if not hasattr(self._hook_module, "hook"):
            return
        hook_api = PostGraphAPI(
            module_name=self.module_name, module_graph=self.module_graph
        )
        self._hook_module.hook(hook_api)
        self.datas.update(set(hook_api._added_datas))
        self.binaries.update(set(hook_api._added_binaries))
        self.hiddenimports.extend(hook_api._added_imports)
        for deleted_module_name in hook_api._deleted_imports:
            self.module_graph.removeReference(hook_api.node, deleted_module_name)

    def _process_hidden_imports(self):
        """
        Add all imports listed in this hook script's `hiddenimports` attribute
        to the module graph as if directly imported by this hooked module.
        
        These imports are typically _not_ implicitly detectable by PyInstaller
        and hence must be explicitly defined by hook scripts.
        """
        for import_module_name in self.hiddenimports:
            try:
                caller = self.module_graph.findNode(
                    self.module_name, create_nspkg=False
                )
                self.module_graph.import_hook(import_module_name, caller)
            except ImportError:
                logger.warning('Hidden import "%s" not found!', import_module_name)

    def _process_excluded_imports(self):
        """
        'excludedimports' is a list of Python module names that PyInstaller
        should not detect as dependency of this module name.
        
        So remove all import-edges from the current module (and it's
        submodules) to the given `excludedimports` (end their submodules).
        """

        def find_all_package_nodes(name):
            mods = [name]
            name += "."
            for subnode in self.module_graph.nodes():
                if subnode.identifier.startswith(name):
                    mods.append(subnode.identifier)

            return mods

        if not self.excludedimports:
            return
        hooked_mods = find_all_package_nodes(self.module_name)
        for item in set(self.excludedimports):
            excluded_node = self.module_graph.findNode(item, create_nspkg=False)
            if excluded_node is None:
                logger.info("Import to be excluded not found: %r", item)
                continue
                logger.info("Excluding import %r", item)
                imports_to_remove = set(find_all_package_nodes(item))
                for src in hooked_mods:
                    references = set(
                        (
                            node.identifier
                            for node in self.module_graph.getReferences(src)
                        )
                    )
                    for dest in imports_to_remove & references:
                        self.module_graph.removeReference(src, dest)
                        logger.info("  Removing import of %s from module %s", dest, src)


class HooksCache(dict):
    """
    Dictionary mapping from the fully-qualified names of each module hooked by
    at least one hook script to lists of the absolute paths of these scripts.
    
    This `dict` subclass caches the list of all hooks applicable to each module,
    permitting Pythonic mapping, iteration, addition, and removal of such hooks.
    Each dictionary key is a fully-qualified module name. Each dictionary value
    is a list of the absolute paths of all hook scripts specific to that module,
    including both official PyInstaller hooks and unofficial user-defined hooks.
    
    See Also
    ----------
    `_load_file_list()`
        For details on hook priority.
    """

    def __init__(self, hooks_dir):
        """
        Initialize this dictionary.
        
        Parameters
        ----------
        hook_dir : str
            Absolute or relative path of the directory containing hooks with
            which to populate this cache. By default, this is the absolute path
            of the `PyInstaller/hooks` directory containing official hooks.
        """
        super(dict, self).__init__()
        self._load_file_list(hooks_dir)

    def _load_file_list(self, hooks_dir):
        """
        Cache all hooks in the passed directory.
        
        **Order of caching is significant** with respect to hooks for the same
        module, as the values of this dictionary are ordered lists. Hooks for
        the same module will be run in the order in which they are cached.
        Previously cached hooks are always preserved (rather than overidden).
        
        Specifically, any hook in the passed directory having the same module
        name as that of a previously cached hook will be appended to the list of
        hooks for that module name. By default, official hooks are cached
        _before_ user-defined hooks. For modules with both official and
        user-defined hooks, this implies that the former take priority over and
        will be run _before_ the latter.
        
        Parameters
        ----------
        hooks_dir : str
            Absolute or relative path of the directory containing additional
            hooks to be cached. For convenience, tilde and variable expansion
            will be applied to this path (e.g., a leading `~` will be replaced
            by the absolute path of the corresponding home directory).
        """
        hooks_dir = expand_path(hooks_dir)
        if not os.path.isdir(hooks_dir):
            logger.error("Hook directory %r not found", os.path.abspath(hooks_dir))
            return
        hook_files = glob.glob(os.path.join(hooks_dir, "hook-*.py"))
        for hook_file in hook_files:
            hook_file = os.path.abspath(hook_file)
            module_name = os.path.basename(hook_file)[5:-3]
            if module_name in self:
                self[module_name].append(hook_file)
            else:
                self[module_name] = [hook_file]

    def add_custom_paths(self, hooks_dirs):
        """
        Cache all hooks in the list of passed directories.
        
        Parameters
        ----------
        hooks_dirs : list
            List of the absolute or relative paths of all directories containing
            additional hooks to be cached.
        """
        for hooks_dir in hooks_dirs:
            self._load_file_list(hooks_dir)

    def remove(self, module_names):
        """
        Remove all key-value pairs whose key is a fully-qualified module name in
        the passed list from this dictionary.
        
        Parameters
        ----------
        module_names : list
            List of all fully-qualified module names to be removed.
        """
        for module_name in set(module_names):
            if module_name in self:
                del self[module_name]


class AdditionalFilesCache(object):
    """
    Cache for storing what binaries and datas were pushed by what modules
    when import hooks were processed.
    """

    def __init__(self):
        self._binaries = {}
        self._datas = {}

    def add(self, modname, binaries, datas):
        self._binaries[modname] = binaries or []
        self._datas[modname] = datas or []

    def __contains__(self, name):
        return name in self._binaries or name in self._datas

    def binaries(self, modname):
        """
        Return list of binaries for given module name.
        """
        return self._binaries[modname]

    def datas(self, modname):
        """
        Return list of datas for given module name.
        """
        return self._datas[modname]


class ImportHook(object):
    """
    Class encapsulating processing of hook attributes like hiddenimports, etc.
    """

    def __init__(self, modname, hook_filename):
        """
        :param hook_filename: File name where to load hook from.
        """
        logger.info("Processing hook   %s" % os.path.basename(hook_filename))
        self._name = modname
        self._filename = hook_filename
        hook_modname = "PyInstaller_hooks_" + modname.replace(".", "_")
        self._module = importlib_load_source(hook_modname, self._filename)
        self.binaries = set()
        self.datas = set()

    def _process_hook_function(self, mod_graph):
        """
        Call the hook function hook(mod).
        Function hook(mod) has to be called first because this function
        could update other attributes - datas, hiddenimports, etc.
        """
        hook_api = PostGraphAPI(self._name, mod_graph)
        self._module.hook(hook_api)
        self.datas.update(set(hook_api._added_datas))
        self.binaries.update(set(hook_api._added_binaries))
        for item in hook_api._added_imports:
            self._process_one_hiddenimport(item, mod_graph)

        for item in hook_api._deleted_imports:
            mod_graph.removeReference(hook_api.node, item)

    def _process_hiddenimports(self, mod_graph):
        """
        'hiddenimports' is a list of Python module names that PyInstaller
        is not able detect.
        """
        for item in self._module.hiddenimports:
            self._process_one_hiddenimport(item, mod_graph)

    def _process_one_hiddenimport(self, item, mod_graph):
        try:
            caller = mod_graph.findNode(self._name, create_nspkg=False)
            mod_graph.import_hook(item, caller=caller)
        except ImportError:
            logger.warning("Hidden import '%s' not found (probably old hook)", item)

    def _process_excludedimports(self, mod_graph):
        """
        'excludedimports' is a list of Python module names that PyInstaller
        should not detect as dependency of this module name.
        
        So remove all import-edges from the current module (and it's
        submodules) to the given `excludedimports` (end their submodules).
        """

        def find_all_package_nodes(name):
            mods = [name]
            name += "."
            for subnode in mod_graph.nodes():
                if subnode.identifier.startswith(name):
                    mods.append(subnode.identifier)

            return mods

        hooked_mods = find_all_package_nodes(self._name)
        for item in set(self._module.excludedimports):
            excluded_node = mod_graph.findNode(item, create_nspkg=False)
            if excluded_node is None:
                logger.info("Import to be excluded not found: %r", item)
                continue
                logger.info("Excluding import %r", item)
                imports_to_remove = set(find_all_package_nodes(item))
                for src in hooked_mods:
                    references = set(
                        (n.identifier for n in mod_graph.getReferences(src))
                    )
                    for dest in imports_to_remove & references:
                        mod_graph.removeReference(src, dest)
                        logger.warning("  From %s removing import %s", src, dest)

    def _process_datas(self, mod_graph):
        """
        'datas' is a list of globs of files or
        directories to bundle as datafiles. For each
        glob, a destination directory is specified.
        """
        self.datas.update(set(format_binaries_and_datas(self._module.datas)))

    def _process_binaries(self, mod_graph):
        """
        'binaries' is a list of files to bundle as binaries.
        Binaries are special that PyInstaller will check if they
        might depend on other dlls (dynamic libraries).
        """
        self.binaries.update(set(format_binaries_and_datas(self._module.binaries)))

    def update_dependencies(self, mod_graph):
        """
        Update module dependency graph with import hook attributes (hiddenimports, etc.)
        :param mod_graph: PyiModuleGraph object to be updated.
        """
        if hasattr(self._module, "hook"):
            self._process_hook_function(mod_graph)
        if hasattr(self._module, "hiddenimports"):
            self._process_hiddenimports(mod_graph)
        if hasattr(self._module, "excludedimports"):
            self._process_excludedimports(mod_graph)
        if hasattr(self._module, "datas"):
            self._process_datas(mod_graph)
        if hasattr(self._module, "binaries"):
            self._process_binaries(mod_graph)
